﻿using System;
using System.Linq;
using Actya.Core.Content;
using Actya.Core.Infrastructure;
using Actya.Core.Infrastructure.Events;
using Actya.Core.Security;
using Actya.Core.SiteStructure.Events;
using Actya.Core.Widgets;
using Raven.Client;

namespace Actya.Core.SiteStructure
{
	public class SiteService : ISiteService, IEventListener<PageCreated>, IEventListener<PageUpdated>, IEventListener<PageDeleted>
	{
		private readonly IMembershipService _membershipService;
		private readonly IDocumentSession _session;
		private readonly IEventPublisher _events;
		private readonly ICmsContext _cmsContext;
		private readonly IWidgetService _widgetService;

		public SiteService(IMembershipService membershipService, IDocumentSession session, IEventPublisher events, ICmsContext cmsContext, IWidgetService widgetService)
		{
			_membershipService = membershipService;
			_widgetService = widgetService;
			_session = session;
			_events = events;
			_cmsContext = cmsContext;
		}

		public Site CreateSite(string hostUrl, string name, string theme, string adminUsername, string adminPassword)
		{
			// NOTE: ideally, this would be executed in a transaction but that causes trouble with the ASP.NET membership
			// provider when it tries to create a new membership database.

			// Also check if the site doesn't already exist
			var site = FindSiteByHostUrl(hostUrl);
			if (site == null)
			{
				site = new Site { Name = name, DefaultTheme = theme};
				site.HostUrls.Add(hostUrl);
				
				site.Setup();
				_session.Store(site);
				var content = new MultiRegionContent();
				var themeToInstall = this._cmsContext.Host.GetAvailableThemes().SingleOrDefault(t => t.Name == theme);
				if (themeToInstall != null)
				{
					site.InstallTheme(themeToInstall);
					content.InitializeForTheme(themeToInstall);
					var adminUrl = String.IsNullOrEmpty(this._cmsContext.Host.Settings.PagesPrefix) ? "cmsadmin/" : "../cmsadmin/";
					content.SetFirstRegionContent(
						string.Format("<p>Visit the <a href=\"{0}\">CMS administration area</a> to edit this page or other site properties.</p>", adminUrl));
				}
				var homePage = new Page
					            {
					               	Layout = site.PageLayouts.Single(x => x.Name == LayoutNames.HomePage).Id,
									Title = "The site is installed successfully!",
					               	Url = "/",
					               	CreatedBy = adminUsername,
					               	DateCreated = DateTime.Now,
					               	ModifiedBy = adminUsername,
					               	DateModified = DateTime.Now,
									Content = content
					            };
				homePage.Publish();
				_session.Store(homePage);
				var homeSitemapNode = new SitemapNode {Title = "Home", Url = "/", PageId = homePage.Id};
				site.SetHomepage(homeSitemapNode);
				site.SitemapNodes.Add(homeSitemapNode);
				_session.Store(site);
			}
			_membershipService.CreateRole(RoleNames.CmsAdministrators);
			_membershipService.CreateUser(adminUsername, adminPassword, RoleNames.CmsAdministrators);

			_session.SaveChanges();
	
			return site;
		}

		public void UpdateSite(Site site)
		{
			_session.Store(site);
			_events.Publish(new SiteUpdated(site));
			_session.SaveChanges();
		}

		public Site FindSiteByHostUrl(string hostUrl)
		{
			return _session.Query<Site>()
				.Customize(x => x.WaitForNonStaleResults()).SingleOrDefault(s => s.HostUrls.Any(u => u == hostUrl));
		}

		public void AddPageLayoutToSite(Site site, PageLayout pageLayout)
		{
			if (site.PageLayouts.Any(pl => pl.Name == pageLayout.Name))
			{
				throw new ArgumentException(string.Format("A page layout with the name '{0}' already exists.", pageLayout.Name));
			}
			pageLayout.Id = Guid.NewGuid().ToString();
			site.PageLayouts.Add(pageLayout);
			_events.Publish(new PageLayoutCreated(pageLayout));
			UpdateSite(site);
		}

		public void UpdatePageLayout(Site site, PageLayout pageLayout)
		{
			if (site.PageLayouts.Any(pl => pl.Name == pageLayout.Name && pl.Id != pageLayout.Id))
			{
				throw new ArgumentException(string.Format("Another page layout with the name '{0}' already exists.", pageLayout.Name));
			}
			var existingLayout = site.PageLayouts.Single(pl => pl.Id == pageLayout.Id);
			site.PageLayouts.Remove(existingLayout);
			site.PageLayouts.Add(pageLayout);
			_events.Publish(new PageLayoutUpdated(pageLayout));
			UpdateSite(site);
		}

		/// <summary>
		/// Checks if a site already exists in the database. If so, the hostUrl is added to the site.
		/// </summary>
		/// <param name="hostUrl"></param>
		/// <returns></returns>
		public bool VerifyCurrentSiteForHost(string hostUrl)
		{
			var site = _session.Query<Site>().Customize(x => x.WaitForNonStaleResultsAsOfLastWrite()).FirstOrDefault();
			if (site != null)
			{
				if (!site.HostUrls.Contains(hostUrl))
				{
					site.HostUrls.Add(hostUrl);
				}
				_session.SaveChanges();
				return true;
			}
			return false;
		}

		#region event listeners

		// NOTE: should we move event listeners to a separate class?

		/// <summary>
		/// Handle the PageCreated event. Add page to navigation when requested.
		/// </summary>
		/// <param name="eventData"></param>
		public void Handle(PageCreated eventData)
		{
			var page = eventData.Page;
			if (eventData.IncludeInNavigation)
			{
				var site = _cmsContext.Site;
				var parentNode = site.FindSitemapNodeById(eventData.ParentNodeId);
				if (String.IsNullOrEmpty(page.Url))
				{
					page.GenerateUrl(parentNode);
				}
				page.Url = EnsureUniqueUrl(page.Url, page.Id);
				var sitemapNode = new SitemapNode { PageId = page.Id };
				sitemapNode.SetParent(site, parentNode);
				sitemapNode.Url = page.Url;
				sitemapNode.Title = eventData.MenuText;
				if (eventData.IsHomePage)
				{
					site.SetHomepage(sitemapNode);
				}
				UpdateSite(site);
			}
			else
			{
				if (String.IsNullOrEmpty(page.Url))
				{
					page.GenerateUrl(null);
				}
				page.Url = EnsureUniqueUrl(page.Url, page.Id);
			}
		}

		/// <summary>
		/// Handle the PageUpdated event. Update the navigation properties for the changed page.
		/// </summary>
		/// <param name="eventData"></param>
		public void Handle(PageUpdated eventData)
		{
			var page = eventData.Page;
			var site = _cmsContext.Site;
			var sitemapNode = site.FindSitemapNodeByPageId(page.Id);
			if (eventData.IncludeInNavigation)
			{
				var parentNode = site.FindSitemapNodeById(eventData.ParentNodeId);
				if (String.IsNullOrEmpty(page.Url))
				{
					page.GenerateUrl(parentNode);
				}
				page.Url = EnsureUniqueUrl(page.Url, page.Id);
				if (sitemapNode == null)
				{
					sitemapNode = new SitemapNode { PageId = page.Id };
				}
				sitemapNode.SetParent(site, parentNode);
				sitemapNode.Url = page.Url;
				sitemapNode.Title = eventData.MenuText;
				if (eventData.IsHomePage)
				{
					site.SetHomepage(sitemapNode);
				}
			}
			else
			{
				if (String.IsNullOrEmpty(page.Url))
				{
					page.GenerateUrl(null);
				}
				// No navigation, optionally remove existing node
				if (sitemapNode != null)
				{
					site.RemoveNodeFromSitemap(sitemapNode);
				}
				page.Url = EnsureUniqueUrl(page.Url, page.Id);
			}
			// Update url of any secundary sitemapnode that references this page.
			var secundaryNodes = site.FindSitemapNodesByPageId(page.Id).Where(n => n.IsPrimaryPageReference == false);
			foreach (var secundaryNode in secundaryNodes)
			{
				secundaryNode.Url = page.Url;
			}
			UpdateSite(site);
		}

		/// <summary>
		/// Handle the PageDeleted event. Removes all SitemapNodes from navigation that are linked to the deleted page.
		/// </summary>
		/// <param name="eventData"></param>
		public void Handle(PageDeleted eventData)
		{
			var site = _cmsContext.Site;
			var nodesToRemove = site.FindSitemapNodesByPageId(eventData.Page.Id);
			foreach (var sitemapNode in nodesToRemove)
			{
				site.RemoveNodeFromSitemap(sitemapNode);
			}
			UpdateSite(site);
		}

		#endregion

		private string EnsureUniqueUrl(string originalUrl, string pageId)
		{
			const string suffix = "-2";
			if (_session.Query<Page>().Where(p => p.Id != pageId && p.Url == originalUrl).Count() > 0)
			{
				var newUrl = originalUrl.Substring(0, originalUrl.Length - 1) + suffix + "/";
				return EnsureUniqueUrl(newUrl, pageId);
			}
			return originalUrl;
		}
	}
}